
"""
.. module:: rt
.. moduleauthor:: @author: 

The purpose of this module is to control the node startup and runtime.
It contains the NodeService class which manages Microsoft Windows Services and
Runtime, which manages starting and stopping the node.

*Constants*:

.. data:: SERVICE_NAME_PREFIX

    SERVICE_NAME_PREFIX is used for ......

.. data:: SERVICE_DISPLAYNAME_PREFIX

    SERVICE_DISPLAYNAME_PREFIX is the display name registered for the Windows service. 

.. data:: SERVICE_DESCRIPTION

    SERVICE_DESCRIPTION is the description registered for the Windows service. 

.. data:: CONFIGSTATE_INITIALISED_STAGE1

    CONFIGSTATE_INITIALISED_STAGE1 is used for ......

.. data:: CONFIGSTATE_INITIALISED_STAGE2

    CONFIGSTATE_INITIALISED_STAGE2 is used for ......

.. seealso:: 
"""

# Imports
#
import time
import logging
import os
import urlparse
import sys
import uuid
import datetime
import threading
if sys.platform == 'win32':
    import win32serviceutil
    import win32service
    import win32event 
    import winerror
elif sys.platform == 'darwin': # OS/X
    pass
else: # assume Linux
    pass


from node import Console, LoggingLevels
from datastore.database import *
from datastore.resource import *
from datastore.uri import *
from runtime import build_version

# Constants
#
SERVICE_NAME_PREFIX = "HAF eDA Node"
SERVICE_DISPLAYNAME_PREFIX = "HAF eDA Node"
SERVICE_DESCRIPTION = "HAF eDA Node for doing blah"

CONFIGSTATE_INITIALISED_STAGE1 = "1"
CONFIGSTATE_INITIALISED_STAGE2 = "2"

# Classes
#

class StartupException(Exception):
    """
    The purpose of this class is ...

    **Constants**:
    """
    
    # Class constants
    #
    def __init__(self):
        # <purpose>
        #
        # Parameters:
        #     <param 1> :
        #     ...
        #
        
        pass

if sys.platform == 'win32':
    class NodeService(win32serviceutil.ServiceFramework):
        """
        The purpose of this class is to interface with Windows Services.
    
        **Constants**:
        
        .. data:: _svc_name_
        
            _svc_name_ is the name registered for the Windows service. 
        
        .. data:: _svc_display_name_
        
            _svc_display_name_ equals :mod:`rt`.SERVICE_DISPLAYNAME_PREFIX.
        
        .. data:: _svc_description_
        
            _svc_description_ equals :mod:`rt`.SERVICE_DESCRIPTION.
        
        :param args:
        """
        
        _svc_name_ = "NodeService"
        _svc_display_name_ = SERVICE_DISPLAYNAME_PREFIX
        _svc_description_ = SERVICE_DESCRIPTION
    
        def __init__(self, args):
            # Constructor.
            #
            
            # Note: After the service is removed, the DLL containing the message 
            # definitions is unloaded and a more verbose message is seen in the 
            # Windows Event Log.
            #
            self.logger = logging.getLogger('yarra.runtime.rt.NodeService')
            self.logger.log(logging.DEBUG, "")
            
            try:
                win32serviceutil.ServiceFramework.__init__(self, 
                                                [NodeService._svc_name_])
                
                # Create an event which we will use to wait on. 
                # The "service stop" request will set this event.
                self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
            except Exception as e:
                self.logger.exception("%s" % e)
            
        def SvcStop(self):
            """
            This function is called by Windows Service to stop the node service. 
            
            :rtype: None
            """
            
            self.logger.log(logging.DEBUG, "")
            
            # Before we do anything, tell the SCM we are starting the stop 
            # process.
            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
            
            # And set my event.
            #
            win32event.SetEvent(self.hWaitStop)
    
        def SvcDoRun(self):
            """
            This function is called by Windows Service to run the node service. 
            
            :rtype: None
            """
            
            self.logger.log(logging.DEBUG, "")
            
            rt = Runtime()
            rt.run()
            
            self.timeout=60000
            while (True):
                # Wait for service stop signal, if I timeout, loop again
                #
                rc = win32event.WaitForSingleObject(self.hWaitStop, 
                                                    self.timeout)
                
                self.logger.log(logging.DEBUG, 
                    "Checking for STOP from Windows Service...")
                
                # Check to see if self.hWaitStop happened
                #
                if (rc == win32event.WAIT_OBJECT_0):
                    # Stop signal encountered
                    #
                    self.logger.log(logging.DEBUG, 
                        "STOP event received by NodeService")
                    nc = Resource(type='term:base/runtime/node_control')
                    nc.dc_title('Node Control (hardcoded)')
                    rt.spaces.put(
                            Uri('internal:base/runtime/node/shutdown/quit'), 
                            nc, 'cb_node_shutdown')
                    break
            
    
        @staticmethod
        def InstallService(distribution):
            """
            This function installs the node with Windows Service. 
            
            :param distribution: Distribution - distribution configuration.
            """
            
            # Note: The current working directory (CWD) for Windows Services is:
            #
            # - "C:\Windows\system32"
            #
            # so if we look for the datastore in the CWD we won't find it. So, 
            # on startup, we infer the path to the datastore from the path of  
            # the executable.
            
            try:
                win32serviceutil.InstallService(
                    pythonClassString= \
                        win32serviceutil.GetServiceClassString(NodeService), 
                    serviceName=NodeService._svc_name_, 
                    displayName=NodeService._svc_display_name_, 
                    startType=win32service.SERVICE_AUTO_START, 
                    #startType=win32service.SERVICE_DEMAND_START, 
                    exeName=distribution.exe_pathname, 
                    exeArgs=distribution.database_filename,
                    description=NodeService._svc_description_)
                
                logging.log(logging.INFO, "Service (%s) installed." % \
                            NodeService._svc_name_)
            except win32service.error, (hr, fn, msg) :
                if (hr == winerror.ERROR_SERVICE_EXISTS):
                    # Service already exists. Update it.
                    #
                    try:
                        logging.log(logging.INFO, 
                            "Service (%s) already installed. Updating." % \
                            NodeService._svc_name_)
                        
                        s = win32serviceutil.GetServiceClassString(NodeService)
                        win32serviceutil.ChangeServiceConfig(
                             pythonClassString= \
                             s, 
                             serviceName=NodeService._svc_name_, 
                             startType=win32service.SERVICE_AUTO_START, 
                             # startType=win32service.SERVICE_DEMAND_START, 
                             exeName=distribution.exe_pathname,
                             exeArgs=distribution.database_filename,
                             displayName=NodeService._svc_display_name_, 
                             description=NodeService._svc_description_)
    
                        logging.log(logging.INFO, "Service (%s) updated." % \
                                    NodeService._svc_name_)
                    except win32service.error, (hr, fn, msg):
                        logging.exception("Error changing service (%s) " + \
                                          "configuration: %s (%d)" % \
                                          (NodeService._svc_name_, msg, hr))
                        raise NodeServiceException(msg)
                else:
                    logging.exception(
                        "Error installing service (%s): %s (%d)" % \
                        (NodeService._svc_name_, msg, hr))
                    raise NodeServiceException(msg)
        
        @staticmethod
        def RemoveService():
            """
            This function removes the node from Windows Service. 
            """
            
            try:
                win32serviceutil.RemoveService(serviceName= \
                                               NodeService._svc_name_)
                logging.log(logging.INFO, "Service (%s) removed." % \
                            NodeService._svc_name_)
            except win32service.error, (hr, fn, msg) :
                logging.exception("Error removing service (%s): %s (%d)" % \
                                  (NodeService._svc_name_, msg, hr))
                raise NodeServiceException(msg)
elif sys.platform == 'darwin': # OS/X
    pass
else: # assume Linux
    pass


class NodeServiceException(Exception):
    """
    This class represents an exception during Windows Service installation
    or running.
    
    **Constants**:
    
    :param msg:
    """
    
    def __init__(self, msg):
        # Constructor.
        #
        
        self.msg = msg
    
    def __str__(self):
        return self.msg


class Runtime(object):
    """
    Additional code executed in node.
    
    **Constants**:
    
    :param datastore: Datastore instance.
    """
    
    # Class constants
    #
        
    distribution = None
    node_control_semaphore = threading.Semaphore()
    
    def __init__(self, datastore):
        # Constructor.
        #
        
        self.spaces = None
        self.datastore = datastore
        
        self.logger = logging.getLogger('yarra.runtime.rt')
        self.logger.log(logging.DEBUG, "Runtime initialising.")
    
    def get_class_type(self, module_path, class_name):
        """
        This function returns the type of the class. 
        
        :param module_path: <class> or <type>
        :param class_name: <class> or <type>
        :rtype: <return type>
        """
        
        # import the module
        import_string = 'from %s import *' % module_path
        self.logger.log(logging.DEBUG, "executing import: %s", import_string)
        exec import_string
        # get reference to the module
        module = sys.modules[module_path]
        # get a reference to the class
        targetClass = getattr(module, class_name)
        return targetClass
    
    def install(self, configurables, distribution):
        """
        This function the locks the datastore, generates the administration
        password, creates the Root node of the datastore, and writes all 
        configurables to the datastore, including the admin password. 
        
        :param configurables: Configurables - user configuration items.
        :param distribution: Distribution - distribution configuration.
        """
        
        try:
            # generate administration password. We use a random UUID.
            # Display this to administration user.
            adminpassword = str(uuid.uuid4())[0:8]
            configurables.adminpassword_md5_digest = \
                                hashlib.md5(adminpassword).hexdigest()
            Console.display("Administration password is (%s)" % \
                                adminpassword)
           
            dt = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
            
            # update the Root node and write to datastore...
            resources = self.datastore.find_type(
                                    Uri('term:base/datastore/root'))
            if len(resources) == 1:
                res = resources[0]
                self.logger.log(logging.INFO, 
                    "Datastore build %s generated at %s",
                    res.datastore_revision, res.datastore_timestamp)
                res.node_id = distribution.node_ID
                res.init_revision = build_version.BUILD_REVISION
                res.init_timestamp = str(dt)
                # we allow extra command line options to be added to the root node
                extra = {}
                for arg in configurables.extra:
                    key, value = arg.split(':', 1)
                    extra[key] = value
                res.extra = extra
                self.datastore.put(res)
                #self.logger.log(logging.DEBUG, rs)
            else:
                self.logger.log(logging.ERROR, 
                    "Fatal Error: Expecting a single root resource. %i found.",
                    len(resources))
                #self.datastore.unlock()
                sys.exit(1)

            # create the configuration resource (copy data from the configurables
            # dict.
            res = Resource(type='term:base/runtime/node_configuration')
            res.dc_title('Node Configuration')
            res.set_metadata(Uri('term:base/build/autoload_suffix'), 
                            'node_configuration')
            for key in configurables.__dict__.keys():
                res.set_var(key, configurables.__dict__[key])
            res.state =  CONFIGSTATE_INITIALISED_STAGE1
            self.datastore.put(res)
            self.logger.log(LoggingLevels.TRACE, res)
        finally:
            #self.datastore.unlock()
            pass
        sys.exit(0)        

    def install_service(self, distribution):
        """
        This function installs the node as a Windows Service. 
        
        :param distribution: Distribution
        """
        
        self.logger.log(logging.DEBUG, "")
        
        try:
            NodeService.InstallService(distribution)
            self.logger.log(logging.INFO, "Service installed.")
        except Exception as e:
            self.logger.log(logging.ERROR, "Error installing service: %s", e)
    
    def remove_service(self):
        """
        This function remove the node as a Windows service.
        """
        
        self.logger.log(logging.DEBUG, "")
        
        try:
            NodeService.RemoveService()
            self.logger.log(logging.INFO, "Service removed.")
        except Exception as e:
            self.logger.log(logging.ERROR, "Error removing service: %s", e)
        
    def start_service_manager_and_config_service(self):
        """
        This is used below in run, and as part of service testing.
        """
        
        from runtime.nodelogger import NodeLogger
        NodeLogger.load()
        
        # start each service as it is added to spaces.
        from services.service_manager_service import ServiceManagerService
        sms = ServiceManagerService(self.spaces, self.datastore,
            Uri('internal:base/services/service_manager_service'), 
            node_start=True)
        sms.start()
            
        # Note: The config_service does not have a service resource created 
        # automatically so one must be added so that it can be controlled.
        service_uri = \
            Uri('internal:base/services-list/base/services/config_service')
        service_res = Resource(type='term:base/runtime/service')
        service_res.dc_title('Config Service (hardcoded)')
        self.spaces.put(service_uri, service_res, warning=False)

        # process each configuration file.
        from services.config_service import ConfigService
        service = ConfigService(self.spaces, self.datastore, service_uri,
            node_start=True)
        service.start()

    def run(self):
        """
        This function is the mainline of Runtime regardless of whether we're
        started as a service or from a shell.
        
        It starts service_manager_service, which starts each service as its 
        definition is added to spaces, and config_service to process each 
        configuration.xml file.
        """
        
        from datastore.database import DataStore
        from datastore.spaces import Spaces
        
        self.logger.log(logging.DEBUG, "")
        self.spaces = Spaces()
        
        try:
            self.start_service_manager_and_config_service()
            # semaphore is released when we want to stop or restart
            Runtime.node_control_semaphore.acquire(True)
            Runtime.node_control_semaphore.acquire(True)
        except:
            self.logger.exception(
               "Runtime caught error (%s)", sys.exc_info()[1])         
        finally:
            #self.datastore.unlock()
            self.datastore.close()
            
        res = self.spaces.get(Uri('internal:base/node_control'))
        if res is None:
            # assume restart (but wait a little to limit the damage if this
            # ends up being a restart error loop).
            self.logger.log(logging.CRITICAL,
                    "Uncaught exception - cannot restart. Shutting down.\n")
            return 20
        else:
            if res.get_var('action', '') == 'restart':
                self.logger.log(logging.INFO, "Attempting node restart.\n\n")
                # release the datastore lock so that we can open and lock it again. 
                self.datastore = None
                self.spaces = None
                return 10
            else:
                self.logger.log(logging.INFO, "Node shutdown complete.")
                return 20
    
    def start_process(self, distribution):
        """
        This function is starts Runtime for a command-line start.
        
        :param distribution: Distribution - distribution configuration.
        """
        
        self.logger.log(logging.DEBUG, "")   
        Runtime.distribution = distribution
        sys.exit(self.run())
        
        # TODO: Tie this shutdown request to Ctrl-C
    
    def start_service(self, distribution):
        """This function starts Runtime as a Service.
        
        :param distribution: Distribution - distribution configuration.
        """
        
        self.logger.log(logging.DEBUG, "")
        Runtime.distribution = distribution
        
        # We have been started from Windows Services...
        import servicemanager
        servicemanager.PrepareToHostSingle(NodeService)
    
        try:
            servicemanager.StartServiceCtrlDispatcher()
        except:
            self.logger.exception("Unable to start as service: %s" % \
                                  sys.exc_info()[1])
    
